using System;
using System.Collections.Generic;
using WDFactoryLib;

namespace CardsEngine.Poker
{

	public abstract class PokerGame: CardGame
	{

		///<summary>Enumerates the types of actions which may be performed in Poker:
		/// Check, Fold, Call, and Bet/Raise</summary>
		public enum PokerActionType
		{ CHECK, FOLD, CALL, BET_OR_RAISE }

		///<summary>Represents an action taken by a player in a poker game</summary>
		public abstract class PokerAction
		{
			private CardsPlayer actor;
			private PokerGame game;
			protected decimal amount;
			protected PokerActionType actionType;

			public PokerAction(PokerGame _game, CardsPlayer _actor)
			{
				game = _game;
				actor = _actor;
			}

			public PokerAction(PokerGame _game, CardsPlayer _actor, PokerActionType _action, decimal _amount)
			{
				game = _game;
				actor = _actor;

				SetStrict(_action, _amount);
			}

			/// <summary>
			/// Get a reference to the game in which this action takes place.
			/// </summary>
			/// <description>
			/// This field is immutable.
			/// </description>
			/// <value>
			/// The game in which this action takes place.
			/// </value>
			public PokerGame Game {
				get { return game; }
			}

			/// <summary>
			/// Gets the type of action to be performed.
			/// </summary>
			/// <description>
			/// This accessor is read-only; to change the value of this field, use
			/// the SetSimple() or SetStrict() methods.
			/// </description>
			/// <value>
			/// The action.
			/// </value>
			public PokerActionType ActionType {
				get { return actionType; }
			}

			/// <summary>
			/// Get a reference to the CardsPlayer who is performing the action.
			/// </summary>
			/// <description>
			/// This field is immutable.
			/// </description>
			/// <value>
			/// The actor.
			/// </value>
			public CardsPlayer Actor
			{
				get { return actor; }
			}

			/// <summary>
			/// Gets the amount of money to be deducted from the player's bank and added to the pot when this action is completed.
			/// </summary>
			/// <description>
			/// Value is always zero for Check and Fold actions, and always positive for
			/// Call and Bet-or-Raise actions.
			/// This accessor is read-only; to change the value of this field, use
			/// the SetSimple() or SetStrict() methods.
			/// </description>
			/// <value>
			/// The amount.
			/// </value>
			public decimal Amount {
				get { return amount; }
			}

            /// <summary>
			/// A simpler way to set the values of the Action and Amount instance fields. 
			/// Automatically determines and assigns the appropriate value for the "_amount" parameter
			/// for all PokerActionTypes other than BET_OR_RAISE, which must be specified manually.
			/// </summary>
			/// <exception cref="System.ArgumentException">The action is BET_OR_RAISE, and in invalid value was specified for the amount</exception>
			/// <param name='_action'>
			/// The action to be performed.
			/// </param>
            public void SetSimple(PokerActionType _action)
            {
                SetStrict(_action, Decimal.Zero);
            }

			/// <summary>
			/// A simpler way to set the values of the Action and Amount instance fields. 
			/// Automatically determines and assigns the appropriate value for the "_amount" parameter
			/// for all PokerActionTypes other than BET_OR_RAISE, which must be specified manually.
			/// </summary>
			/// <exception cref="System.ArgumentException">The action is BET_OR_RAISE, and in invalid value was specified for the amount</exception>
			/// <param name='_action'>
			/// The action to be performed.
			/// </param>
			/// <param name='_amount'>
			/// The amount of money to be placed into the pot by this action. Value is
			/// ignored and automatically replaced unless the action type is BET_OR_RAISE.
			/// </param>
			public abstract void SetSimple(PokerActionType _action, decimal _amount);
				
            /// <summary>
			/// Sets the values of the Action and Amount instance fields. Various restrictions apply.
			/// </summary>
			/// <description>
			/// The SetSimple() method offers a simpler alternative that is more appropriate for most applications.
			/// An exception occurs if any of the following criteria are met:
			/// (1) action is CHECK or FOLD and amount is non-zero; 
			/// (2) action is CALL and amount is not equal to this.Game.CurrentBetToMatch
			/// (3) action is BET_OR_RAISE and amount is less or equal to this.Game.CurrentBetToMatch
			/// </description>
			/// <exception cref="System.ArgumentException">The specified amount is invalid, given the nature of the specified action</exception>
			/// <param name='_action'>
			/// The action to be performed
			/// </param>
            public void SetStrict(PokerActionType _action)
            {
                SetStrict(_action, Decimal.Zero);
            }

			/// <summary>
			/// Sets the values of the Action and Amount instance fields. Various restrictions apply.
			/// </summary>
			/// <description>
			/// The SetSimple() method offers a simpler alternative that is more appropriate for most applications.
			/// An exception occurs if any of the following criteria are met:
			/// (1) action is CHECK or FOLD and amount is non-zero; 
			/// (2) action is CALL and amount is not equal to this.Game.CurrentBetToMatch
			/// (3) action is BET_OR_RAISE and amount is less or equal to this.Game.CurrentBetToMatch
			/// </description>
			/// <exception cref="System.ArgumentException">The specified amount is invalid, given the nature of the specified action</exception>
			/// <param name='_action'>
			/// The action to be performed
			/// </param>
			/// <param name='_amount'>
			/// The amount of money to be deducted from the player's bank and added to the pot when this action is completed
			/// </param>
			public abstract void SetStrict(PokerActionType _action, decimal _amount);
		}

		///<summary>Represents a type of poker action for which the ActionType and Amount fields
		/// are writable via the SetSimple and SetStrict methods.</summary>
		public sealed class MutablePokerAction: PokerAction
		{
			public MutablePokerAction(PokerGame _game, CardsPlayer _actor)
				:base(_game, _actor)
			{ }

			public static implicit operator ImmutablePokerAction (MutablePokerAction mpa)
			{
				return new ImmutablePokerAction(mpa);
			}

			/// <summary>
			///  A simpler way to set the values of the Action and Amount instance fields. Automatically determines and assigns
			/// the appropriate value for the "_amount" parameter for all PokerActionTypes other than BET_OR_RAISE, which must be
			/// specified manually. 
			/// </summary>
			/// <exception cref='System.ArgumentException'>
			/// The action is BET_OR_RAISE, and in invalid value was specified for the amount
			/// </exception>
			/// <param name='_action'>
			///  The action to be performed. 
			/// </param>
			/// <param name='_amount'>
			///  The amount of money to be placed into the pot by this action. Value is ignored and automatically replaced unless
			/// the action type is BET_OR_RAISE. 
			/// </param>
			public override void SetSimple (PokerActionType _action, decimal _amount)
			{
				switch (_action)
				{
				case PokerActionType.FOLD:
					goto case PokerActionType.CHECK;

				case PokerActionType.CHECK:
					_amount = 0;
					break;

				case PokerActionType.CALL:
					_amount = Game.CurrentBetToMatch;
					break;
				}

				SetStrict(_action, _amount);
			}

			/// <summary>
			///  Sets the values of the Action and Amount instance fields. Various restrictions apply. 
			/// </summary>
			/// <description>
			///  The SetSimple() method offers a simpler alternative that is more appropriate for most applications. An exception
			/// occurs if any of the following criteria are met: (1) action is CHECK or FOLD and amount is non-zero; (2) action
			/// is CALL and amount is not equal to this.Game.CurrentBetToMatch (3) action is BET_OR_RAISE and amount is less or
			/// equal to this.Game.CurrentBetToMatch 
			/// </description>
			/// <exception cref='System.ArgumentException'>
			/// The specified amount is invalid, given the nature of the specified action
			/// </exception>
			/// <param name='_action'>
			///  The action to be performed 
			/// </param>
			/// <param name='_amount'>
			///  The amount of money to be deducted from the player's bank and added to the pot when this action is completed 
			/// </param>
			public override void SetStrict (PokerActionType _action, decimal _amount)
			{

				switch (_action) {
				case PokerActionType.BET_OR_RAISE:
					if (_amount <= Game.CurrentBetToMatch)
						throw new ArgumentException(
							"For bet-or-raise actions, the \"_amount\" parameter must have a value greater than " +
							"this(PokerAction).Game.CurrentBetToMatch");

					break;

				case PokerActionType.CALL:
					if (_amount != Game.CurrentBetToMatch)
						throw new ArgumentException(
							"For call actions, the \"_amount\" parameter must have a value equal to " +
							"this(PokerAction).Game.CurrentBetToMatch");
					break;

				case PokerActionType.CHECK:
					goto case PokerActionType.FOLD;

				case PokerActionType.FOLD:
					if (_amount != 0)
						throw new ArgumentException("For check and fold actions, the value of the \"_amount\" parameter must equal zero");
					break;

				default:
					throw new NotImplementedException();
				}

				this.actionType = _action;
				this.amount = _amount;
			}
		}

		///<summary>Represents an immutable action in a poker game, i.e. an action which has already been fully determined</summary>
		public sealed class ImmutablePokerAction: PokerAction
		{
			public ImmutablePokerAction(PokerAction pa)
				:base(pa.Game, pa.Actor, pa.ActionType, pa.Amount)
			{ }

			/// <summary>
			/// Throws a NotSupportedException, as SetStrict may not be called on an instance of ImmutablePokerAction
			/// </summary>
			/// <exception cref='System.InvalidOperationException'>
			/// Always thrown.
			/// </exception>
			/// <param name='_action'>
			///  The action to be performed 
			/// </param>
			/// <param name='_amount'>
			///  The amount of money to be deducted from the player's bank and added to the pot when this action is completed 
			/// </param>
			public override void SetStrict(PokerActionType _action, decimal _amount)
			{
				throw new InvalidOperationException("Cannot call SetStrict on an instance of ImmutablePokerAction");
			}

			/// <summary>
			/// Throws a NotSupportedException, as SetSimple may not be called on an instance of ImmutablePokerAction
			/// </summary>
			/// <exception cref='System.InvalidOperationException'>
			/// Always thrown.
			/// </exception>
			/// <param name='_action'>
			///  The action to be performed 
			/// </param>
			/// <param name='_amount'>
			///  The amount of money to be deducted from the player's bank and added to the pot when this action is completed 
			/// </param>
			public override void SetSimple (PokerActionType _action, decimal _amount)
			{
				throw new InvalidOperationException("Cannot call SetSimple on an instance of ImmutablePokerAction");
			}
		}

		///<summary>Represents a "pot" winnable by a set of players in a Poker game</summary>
		protected class Pot
		{
			/// <summary>
			/// The total value of the pot.
			/// </summary>
			public decimal Value;

			/// <summary>
			/// The eligible winners of the pot.
			/// </summary>
			public IList<CardsPlayer> EligibleWinners;

			public Pot(IList<CardsPlayer> winners_inject)
			{
				EligibleWinners = winners_inject;
			}
		}

		private IDictionary<CardsPlayer,decimal> investedInCurrentBettingRound;
		private decimal currentBetToMatch;
		private PokerSettings settings;
		private IList<Pot> pots;
		private IList<ImmutablePokerAction> actionHistory;
		private IDictionary<Pot,IEnumerable<CardsPlayer>> winningsMatrix;

		/// <summary>
		/// The function which is used to obtain a player's choice of action in a poker game
		/// </summary>
		public Func<MutablePokerAction, ImmutablePokerAction> GetPlayerAction;

		public PokerGame ()
			:base()
		{
			settings = PokerSettings.GetDefault();
			InitializeInstance();
		}

		public PokerGame (PokerSettings _settings)
			:base ()
		{
			settings = _settings;
			InitializeInstance ();
		}

        public PokerGame(IDeckGenerator _deckGenerator)
            : base(_deckGenerator)
        {
            settings = PokerSettings.GetDefault();
            InitializeInstance();
        }

        public PokerGame(IDeckGenerator _deckGenerator, PokerSettings _settings)
            : base(_deckGenerator)
        {
            settings = _settings;
            InitializeInstance();
        }

        internal PokerGame(WDFactoryLib.IFactory _factory)
            : base()
        {
            settings = PokerSettings.GetDefault();
            InitializeInstance();
        }

        internal PokerGame(WDFactoryLib.IFactory _factory, PokerSettings _settings)
			:base()
		{
			settings = _settings;
			InitializeInstance();
		}

        internal PokerGame(WDFactoryLib.IFactory _factory, IDeckGenerator _deckGenerator)
			:base(_factory, _deckGenerator)
		{
			settings = PokerSettings.GetDefault();
			InitializeInstance();
		}

		internal PokerGame (WDFactoryLib.IFactory _factory, PokerSettings _settings, IDeckGenerator _deckGenerator)
			:base(_factory, _deckGenerator)
		{
			settings = _settings;
			InitializeInstance();
		}

		#region Public Accessors

		/// <summary>
		/// Gets the action history.
		/// </summary>
		/// <value>
		/// The action history.
		/// </value>
		public IList<ImmutablePokerAction> ActionHistory
		{
			get { return actionHistory; }
		}

		/// <summary>
		/// Gets the total value of the main pot.
		/// </summary>
		/// <value>
		/// The main pot value.
		/// </value>
		public decimal MainPotValue
		{
			get { return MainPot.Value; }
		}

		/// <summary>
		/// Gets the current bet to match.
		/// </summary>
		/// <value>
		/// The current bet to match.
		/// </value>
		public decimal CurrentBetToMatch
		{
			get { return currentBetToMatch; }
		}

		/// <summary>
		/// Compute and return the total value of the main pot plus any & all side pots.
		/// </summary>
		/// <description>Time complexity O(n) where n = the number of side pots.</description>
		/// <returns>
		/// The total pot value.
		/// </returns>
		public decimal TotalPotValue ()
		{
			decimal result = 0;

			foreach (Pot p in pots)
				result += p.Value;

			return result;
		}

		#endregion

		#region Protected Accessors
		/// <summary>
		/// Gets the 'main' pot, i.e. the one which is presently
		/// receiving bets
		/// </summary>
		/// <value>
		/// The main pot.
		/// </value>
		protected Pot MainPot
		{
			get { return pots [0]; }
		}

		/// <summary>
		/// Gets the winnings matrix. To ensure a non-null value,
		/// call ComputeWinningsMatrix() prior to using this accessor.
		/// </summary>
		/// <value>
		/// The winnings matrix.
		/// </value>
		protected IDictionary<Pot,IEnumerable<CardsPlayer>> WinningsMatrix {
			get { return winningsMatrix; }
		}

		#endregion

		private void InitializeInstance()
		{
			base.GameStarting += StartPokerGame;
			base.GameEnding += EndPokerGame;
		}

        /// <summary>
        /// Prefer the overload that accepts a CardsPlayer instead, when possible
        /// </summary>
        /// <param name="pl"></param>
        /// <returns></returns>
        public decimal GetAmountNeededToCall(Player pl)
        {
            CardsPlayer match = null;

            foreach (CardsPlayer p in Players)
                if (p.Owner == pl)
                {
                    match = p;
                    break;
                }

            return GetAmountNeededToCall(match);
        }

        public decimal GetAmountNeededToCall(CardsPlayer pl)
        {
            return (CurrentBetToMatch - investedInCurrentBettingRound[pl]);
        }

		#region Player-Action Methods

		private void InitializeBettingRound ()
		{
			currentBetToMatch = 0;
			investedInCurrentBettingRound = Factory.GetNewDictionary<CardsPlayer, decimal>();

			foreach (CardsPlayer x in MainPot.EligibleWinners)
				investedInCurrentBettingRound[x] = 0;
		}

		private void GenerateNewSidePot (CardsPlayer whom)
		{
			Pot newPot = new Pot(Factory.GetNewList<CardsPlayer>(MainPot.EligibleWinners));
			newPot.EligibleWinners.Remove(whom);
			pots.Insert(0,newPot);
		}

		/// <summary>
		/// Performs a round of betting
		/// </summary>
		/// <description>Time Complexity O(n) where n = MainPot.EligibleWinners.Count</description>
		protected virtual void PerformBettingRound ()
		{
			InitializeBettingRound();
			foreach (CardsPlayer cplayer in MainPot.EligibleWinners)
				PerformAction(GetPlayerAction.Invoke(new MutablePokerAction(this,cplayer)));
		}

		private void VerifyFunds (Player player, decimal amount)
		{
			if (player.Bank < amount)
				throw new ArgumentException(String.Format("Player {0} does not have sufficient funds to make a bet in the amount of {0}", player.Name, amount));
			else if (settings.limit_type == PokerSettings.LimitType.POT_LIMIT && amount > TotalPotValue ())
				throw new ArgumentException (String.Format ("Bet amount ({0}) exceeds the pot-limit; the current maximum for this bet is {1}", amount, TotalPotValue ()));
			else if (settings.limit_type == PokerSettings.LimitType.SIMPLE_FIXED_LIMIT && amount > CurrentBetToMatch + settings.bet_limit)
				throw new ArgumentException (String.Format ("Bet amount ({0}) exceeds the fixed limit of {1} for this game", amount, settings.bet_limit));
		}

		private void VerifyFunds (CardsPlayer player, decimal amount)
		{
			VerifyFunds(player.Owner, amount);
		}

		private void AddFundsToPot (CardsPlayer from_whom, decimal amount)
		{
			from_whom.Owner.Bank -= amount;
			MainPot.Value += amount;

			investedInCurrentBettingRound[from_whom] += amount;
		}

		private void PerformBetOrRaise (CardsPlayer actor, decimal amount)
		{
			VerifyFunds(actor, amount);

			decimal newTotalInvestment = investedInCurrentBettingRound[actor] + amount;

            // Allow bet/raise to occur even when the amount is inappropriate according to the rules of poker
            // This is weak in terms of security and could benefit from improvement,
            //  however it might be difficult to catch exceptions thrown by the Perform* methods
            /*
			if (newTotalInvestment <= CurrentBetToMatch)
				throw new ArgumentException(String.Format("Action type is \"BET_OR_RAISE\" yet the amount specified {0} is less-or-equal to the expected minimum {1}",amount, CurrentBetToMatch));
            */

			AddFundsToPot(actor, amount);
		}

		private void PerformCall (CardsPlayer actor, decimal amount)
		{
			VerifyFunds(actor, amount);

			bool newSidepot = false;
			decimal newTotalInvestment = investedInCurrentBettingRound[actor] + amount;

			if (newTotalInvestment != CurrentBetToMatch) {
                // Allow call to occur even when it is inappropriate according to poker rules
                // This is weak in terms of security and could benefit from improvement,
                //  however it might be difficult to catch exceptions thrown by the Perform* methods
                /*
				if (newTotalInvestment > CurrentBetToMatch)
					throw new ArgumentException (String.Format ("Action type is \"CALL\" yet the amount specified {0} is not equal to the expected amount", amount));
				else {
					if (amount < actor.Owner.Bank)
						throw new ArgumentException (String.Format ("Action type is \"CALL\" yet the amount specified {0} is not equal to the expected amount", amount));
					else
                 */
						newSidepot = true;
				//}
			}

			AddFundsToPot(actor, amount);

			if (newSidepot)
				GenerateNewSidePot (actor);
		}

		private void PerformFold (CardsPlayer actor)
		{

			foreach (Pot p in pots) {
				p.EligibleWinners.Remove (actor);
				if (p.EligibleWinners.Count == 1)
				{
					AwardPot(p);
				}
			}
		}

		private void PerformCheck (CardsPlayer actor)
		{
            // Allow check to occur even when it shouldn't, for now.
            // This is weak in terms of security and could benefit from improvement,
            //  however it might be difficult to catch exceptions thrown by the Perform* methods
            /*
            if (investedInCurrentBettingRound[actor] < CurrentBetToMatch)
                throw new InvalidOperationException("Player not allowed to check - has not invested the minimum amount in the current betting round");
            */
		}

		private void PerformAction (PokerAction action)
		{
			CardsPlayer actor = action.Actor;

            // Allow action to occur even when the player shouldn't be acting at all, for now.
            // This is weak in terms of security and could benefit from improvement,
            //  however it might be difficult to catch exceptions thrown by the Perform* methods
            /*
			if (!MainPot.EligibleWinners.Contains(actor))
				throw new ArgumentException(string.Format("PerformAction called for an Actor ({0}) who is not an eligible winner of the main pot - perhaps this player has already folded",actor));
            */

			switch (action.ActionType) {
			case PokerActionType.CHECK:
				PerformCheck (actor);
				break;

			case PokerActionType.FOLD:
				PerformFold (actor);
				break;

			case PokerActionType.CALL:
				PerformCall (actor, action.Amount);
				break;

			case PokerActionType.BET_OR_RAISE:
				PerformBetOrRaise (actor, action.Amount);
				break;
			}

			actionHistory.Add((ImmutablePokerAction)action);
		}

		private void AwardPot(Pot p)
		{
			if (p.EligibleWinners.Count != 1)
				throw new ArgumentException("The specified pot must have exactly one eligible winner when passed to AwardPot");

			p.EligibleWinners[0].Owner.Bank += p.Value;
		}

		/*
		 * "Award Win By Default" method
		// TODO - Method Needs Work
		// Called as soon as the number of eligible winners for the main pot
		// drops to 1 (because other players have folded out).
		// DO NOT FORGET TO TAKE SIDEPOTS INTO ACCOUNT AS SIDEPOTS MAY STILL HAVE MORE THAN
		// ONE ELIGIBLE WINNER
		private void AwardWinByDefault ()
		{
			if (MainPot.EligibleWinners.Count != 1)
				throw new InvalidOperationException("AwardWinByDefault may only be called when there is only one eligible winner for the main pot");

			// Weird Compiler Error!?
			//GameEnding.Invoke(new CardGame.Args(this));

			// Look through the side pots.
			foreach (Pot p in pots)
			{
				// Skip evaluation of the main pot - we already know its winner
				if (p == MainPot)
					continue;

				// TODO
				if (p.EligibleWinners.Count > 1)
					throw new NotImplementedException("TODO - Implement support for AwardWinByDefault when there are sidepots with more than 1 eligible winner");

				else
					p.EligibleWinners[0].Owner.Bank += p.Value;
			}
		}
		*/

		#endregion

		/// <summary>
		/// Computes the current winnings matrix, storing the result in
		/// this.WinningsMatrix
		/// </summary>
		protected void ComputeWinningsMatrix ()
		{
			winningsMatrix = Factory.GetNewDictionary<Pot,IEnumerable<CardsPlayer>>();

			foreach (Pot p in pots)
				winningsMatrix[p] = ComputeWinners(p);
		}

		//Implemented using System.Collections.Generic.List<T>
		// Performance of List.Add: O(1)
		// Performance of List.Clear: O(n)
		private IEnumerable<CardsPlayer> ComputeWinners (Pot pot)
		{
			ulong currentHandValue;
			ulong bestHandValue = 0;
			ICollection<CardsPlayer> winners = Factory.GetNewList<CardsPlayer>(1);

			foreach (CardsPlayer player in pot.EligibleWinners) {
				currentHandValue = ComputeHandValue(player.Hand);

				if (currentHandValue >= bestHandValue) {

					if (currentHandValue > bestHandValue)
					{
						winners.Clear();
						bestHandValue = currentHandValue;
					}

					winners.Add(player);
				}
			}

			return winners;
		}

		//Todo
		protected virtual ulong ComputeHandValue (ICollection<Card> hand)
		{
			throw new NotImplementedException();
		}

		private static void StartPokerGame (CardGame.Args args)
		{
			PokerGame game;

			try {
				game = (PokerGame)args.Game;
			} catch (InvalidCastException ex) {
				throw new ArgumentException("Invalid parameter 'args': Could not cast args.Game to PokerGame (THIS SHOULD NEVER HAPPEN)", ex);
			}

            if (game.GetPlayerAction == null)
                throw new InvalidOperationException("Cannot start the game while the GetPlayerAction field has a null value");
            /*
            if (game.GetPlayerAction == null)
                game.GetPlayerAction = (new PokerTestKit.TextBasedActionHandler()).GetPlayerAction;
            */

			game.winningsMatrix = null;
			game.actionHistory = game.Factory.GetNewList<ImmutablePokerAction>();
			game.pots = game.Factory.GetNewList<Pot>();

			Pot pot = new Pot(game.Factory.GetNewList<CardsPlayer>());
			pot.Value = 0;

			foreach (CardsPlayer player in game.Players)
				pot.EligibleWinners.Add(player);

			game.pots.Add(pot);
		}

		private static void EndPokerGame (CardGame.Args _args)
		{

		}
	}
}

